/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **************************************************************************
 * MIFSS  - content storage system
 * 
 *
 * @uthors: uros.kristan@gmail.com (Uroš Kristan ) Urosk.NET
 *         jernej.svigelj@gmail.com (Jernej Švigelj) 
 */
package net.monare.mifss.managers;

import net.monare.mifss.configurations.pojo.ContentMetaDataDef;
import net.monare.mifss.configurations.pojo.ContentPoolDef;
import net.monare.mifss.configurations.pojo.StorageDef;
import net.monare.mifss.exceptions.*;
import net.monare.mifss.lib.StorageProgressWalker;
import net.monare.mifss.lib.db.DataResult;
import net.monare.mifss.workers.interfaces.ContentMetaDataDao;
import net.monare.mifss.workers.interfaces.ContentPoolHandler;
import net.monare.mifss.workers.interfaces.FileHandler;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Component
public class ContentManager {

    private static Logger logger = Logger.getLogger(ContentManager.class);

    @Autowired
    private ContentMetaDataDao contentMetaDataDao;

    @Autowired
    private FileHandler fileHandler;

    @Autowired
    private PoolManager poolManager;

    @Autowired
    private StorageManager storageManager;

    @Autowired
    private TransformationManager transformationManager;

    /**
     * Returns outpustStream for given uuid and storage
     *
     * @param uuid
     * @param storageName
     * @return
     * @throws ContentManagerException
     */
    public void retriveContent(File outFile, String uuid, String storageName, boolean verifyContent) throws ContentManagerException {

        StorageDef storageDef;

        try {
            storageDef = storageManager.getStorageDefinition(storageName);

            // load meta data from database
            ContentMetaDataDef contentMetaDataDef = contentMetaDataDao.getContentMetaDefFromUUID(uuid, storageDef);

            // get pool impl
            int idPool = contentMetaDataDef.getIdContentPool();
            ContentPoolDef contentPoolDef = storageDef.getContentPoolDefFromId(idPool);

            // load actual content from pool
            ContentPoolHandler contentPoolHandler = poolManager.getContentPoolImplementation(contentPoolDef);
            contentPoolHandler.getFile(outFile, contentMetaDataDef);

            // verify
            if (verifyContent) {

                String calaculatedHashValue = fileHandler.getHashForFile(outFile);
                String storedHashValue = contentMetaDataDef.getFileHash();

                if (!calaculatedHashValue.equals(storedHashValue)) {

                    String errMessage = "Content for UUID " + uuid + " (storage name:" + contentMetaDataDef.getStorageName() + " pool id:"
                            + contentMetaDataDef.getIdContentPool() + ") is corupted. Hash value missmatch.";

                    logger.error(errMessage);
                    throw new ContentManagerException(errMessage);

                } else {
                    logger.info("Content is valid. Calculated hash == stored hash. Hash value:" + calaculatedHashValue);
                }

            }

        } catch (StorageManagerException e) {

            String errMessage = "Error occured while working with storage manager: " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        } catch (ContentMetaDataDaoException e) {

            String errMessage = "Error occured while working with database acces: " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        } catch (PoolManagerException e) {

            String errMessage = "Error occured while working with pool manager: " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        } catch (ContentPoolHandlerException e) {

            String errMessage = "Error occured while working with content: " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        } catch (FileHandlerException e) {

            String errMessage = "Error occured while working files: " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);
        }


    }

    public List<ContentMetaDataDef> retriveContentMetaData(String uuid) throws ContentManagerException {

        List<ContentMetaDataDef> result = new ArrayList<>();

        try {

            //
            // search for renditions in all defined storages

            for (StorageDef sDef : storageManager.listStorages()) {

                //
                // load meta data from database

                ContentMetaDataDef cDef = contentMetaDataDao.getContentMetaDefFromUUID(uuid, sDef);

                //
                // add metadata to result list
                if (cDef != null) {
                    result.add(cDef);
                }

            }

        } catch (StorageManagerException e) {

            String errMessage = "Error occured while trying to list storages " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        } catch (ContentMetaDataDaoException e) {

            String errMessage = "Error occured while trying to get content meta data definiton ... " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        }

        return result;
    }

    /**
     * @param uuid
     * @param storageName
     * @return
     * @throws ContentManagerException
     */
    public ContentMetaDataDef retriveContentMetaData(String uuid, String storageName) throws ContentManagerException {

        StorageDef storageDef;
        try {

            storageDef = storageManager.getStorageDefinition(storageName);

            if (storageDef == null) {
                String errMessage = "Storage with name " + storageName + " not found.";
                logger.error(errMessage);
                throw new ContentManagerException(errMessage);
            }

            // load meta data from database
            ContentMetaDataDef contentMetaDataDef = contentMetaDataDao.getContentMetaDefFromUUID(uuid, storageDef);

            return contentMetaDataDef;

        } catch (StorageManagerException e) {

            String errMessage = "Error occured while trying to get storage definition for storage name : " + storageName + " : " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        } catch (ContentMetaDataDaoException e) {

            String errMessage = "Error occured while trying to get content meta data definiton :" + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        }
    }

    public ContentMetaDataDef writeContent(String fileName, File file, String storageName) throws ContentManagerException {

        if (file == null || !file.exists()) {

            String errMessage = "File doesnt exists!" + file.getAbsolutePath();
            logger.error(errMessage);
            throw new ContentManagerException(errMessage);

        }

        StorageDef storageDef;

        try {

            storageDef = storageManager.getStorageDefinition(storageName);

            if (storageDef == null) {

                String errMessage = "Storage with name " + storageName + " doesnt exist";
                logger.error(errMessage);
                throw new ContentManagerException(errMessage);

            }
            if (storageDef.isChained()) {

                String errMessage = "Its not allowed to directly write to CHAINED stoage!";
                logger.error(errMessage);
                throw new ContentManagerException(errMessage);

            }

            return writeContent(fileName, file, storageDef, null);

        } catch (StorageManagerException e) {

            String errMessage = "Error occured while trying to get storage definiton: " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        }

    }

    private void mapper(String passedFileName, ContentMetaDataDef contentMetaDataDef, File inputFile, File transformedFile)
            throws ContentManagerException {

        File file = null;

        String fileName = null;

        if (transformedFile != null) {
            file = transformedFile;
            contentMetaDataDef.setTransformation(1);
            contentMetaDataDef.setOriginalFilePath(null);
            fileName = fileHandler.replaceFileExtension(inputFile.getName(), fileHandler.getFileExtension(transformedFile.getName()));
        } else {
            file = inputFile;
            contentMetaDataDef.setTransformation(0);
            contentMetaDataDef.setOriginalFilePath(inputFile.getAbsolutePath());
            if (passedFileName != null)
                fileName = passedFileName;
            else
                fileName = inputFile.getName();
        }

        try {

            contentMetaDataDef.setFileName(fileName);

            contentMetaDataDef.setDataSize(fileHandler.getFilesize(file));
            contentMetaDataDef.setExtension(fileHandler.getFileExtension(file.getName()));
            contentMetaDataDef.setFileHash(fileHandler.getHashForFile(file));
            contentMetaDataDef.setFileHashMethod(fileHandler.getHashMethod());

            contentMetaDataDef.setInserted(fileHandler.getCurrentDate());
            contentMetaDataDef.setMetaData(fileHandler.getContentMetaData(file));
            contentMetaDataDef.setMimeType(fileHandler.getMimeType(file));

        } catch (FileHandlerException e) {

            String errMessage = "Error occured while working with files: " + e.getMessage();
            logger.error(errMessage, e);
            throw new ContentManagerException(errMessage, e);

        }

    }

    public ContentMetaDataDef writeContent(String fileName, File inputFile, StorageDef storageDef, String parentUUID)
            throws ContentManagerException {

        ContentPoolHandler contentPoolHandler = null;
        ContentMetaDataDef contentMetaDataDef = null;
        File transformedFile = null;

        try {

            // load actual content from pool
            ContentPoolDef activeContentPoolDef = storageDef.getActivePoolDef();

            // Create new content meta data definition and populate it
            contentMetaDataDef = new ContentMetaDataDef();

            // if child storage then attach
            if (parentUUID == null)
                contentMetaDataDef.setUuid(fileHandler.getUniqueId());
            else
                contentMetaDataDef.setUuid(parentUUID);

            // always
            contentMetaDataDef.setIdContentPool(activeContentPoolDef.getId());
            contentMetaDataDef.setStorageName(storageDef.getName());

            // if transformable file, then try to transform it immediately
            if (storageDef.isTransformable()) {
                transformedFile = transformationManager.transform(storageDef, inputFile);

                // if transformation is not hendable
                if (transformedFile == null) {
                    return null;
                }
            }

            // mapper based on custom rules
            mapper(fileName, contentMetaDataDef, inputFile, transformedFile);

            // insert content to database
            contentMetaDataDef = contentMetaDataDao.insertContentMetaDataDef(contentMetaDataDef, storageDef);

            contentPoolHandler = poolManager.getContentPoolImplementation(activeContentPoolDef);

            // create transformation if transformable
            if (storageDef.isTransformable()) {

                // contentMetaDataDef.setMimeType(fileHandler.getMimeType(inputFile));
                // store trasnformed file to pool
                contentPoolHandler.putFile(transformedFile, contentMetaDataDef);

            } else {
                // store original file to pool if no transforation defined
                contentPoolHandler.putFile(inputFile, contentMetaDataDef);
            }

            // chained storages caller
            List<StorageDef> listChilds = storageManager.getChildStorageDefs(storageDef);

            for (StorageDef childStorageDef : listChilds) {
                if (storageDef.isTransformable()) {
                    writeContent(fileName, transformedFile, childStorageDef, contentMetaDataDef.getUuid());
                } else {
                    writeContent(fileName, inputFile, childStorageDef, contentMetaDataDef.getUuid());
                }
            }

            return contentMetaDataDef;

        } catch (TransformationManagerException e) {
            cleanAfterFailure(contentMetaDataDef, contentPoolHandler, storageDef);
            String errMessage = "Error occured during write content in transformation phase. " + e.getMessage();
            logger.error(errMessage, e);

            throw new ContentManagerException(errMessage, e);

        } catch (ContentMetaDataDaoException e) {
            cleanAfterFailure(contentMetaDataDef, contentPoolHandler, storageDef);
            String errMessage = "Error occured during write content while working with DB. " + e.getMessage();
            logger.error(errMessage, e);

            throw new ContentManagerException(errMessage, e);

        } catch (PoolManagerException e) {
            cleanAfterFailure(contentMetaDataDef, contentPoolHandler, storageDef);
            String errMessage = "Error occured during write content while working with pools. " + e.getMessage();
            logger.error(errMessage, e);

            throw new ContentManagerException(errMessage, e);

        } catch (ContentPoolHandlerException e) {
            cleanAfterFailure(contentMetaDataDef, contentPoolHandler, storageDef);
            String errMessage = "Error occured during write content phase. " + e.getMessage();
            logger.error(errMessage, e);

            throw new ContentManagerException(errMessage, e);
        } finally {

            silentlyDeleteFile(transformedFile);
        }

    }

    protected void silentlyDeleteFile(File file) {
        if (file != null)
            try {

                file.delete();

            } catch (Exception e) {

                String errMessage = "Error occred while deleting file: " + file.getPath() + " . " + e.getMessage();
                logger.error(errMessage, e);

            }
    }

    private void cleanAfterFailure(ContentMetaDataDef contentMetaDataDef, ContentPoolHandler contentPoolHandler, StorageDef storageDef) {

        //
        // remove content form storage pool
        try {

            if (contentPoolHandler != null && contentMetaDataDef != null) {
                contentPoolHandler.deleteFile(contentMetaDataDef);
            }

        } catch (Exception x) {
            logger.error("Error occured while disposing content form pool: " + x.getMessage(), x);
        }

        //
        // remove record from table
        try {

            if (contentMetaDataDef != null) {
                contentMetaDataDao.removeContentMetaData(contentMetaDataDef.getUuid(), storageDef);
            }

        } catch (Exception x) {
            logger.error("Error occured while cleaning table after failed write: " + x.getMessage(), x);
        }

    }

    public void recreateCompleteContentForStorage(String storageName, StorageProgressWalker storageProgressWalker) throws StorageManagerException {

        try {

            StorageDef storageDef = storageManager.getStorageDefinition(storageName);

            // parent storage recreation is not possible
            if (!storageDef.isChained()) {
                String errMessage = "Error! Storage is not chained to ParentStorage. Recreating Parent storages is not possible.";
                logger.error(errMessage);
                throw new StorageManagerException(errMessage);
            }

            StorageDef parentStorageDef = storageManager.getStorageDefinition(storageDef.getChainedToStorageName());

            String storageSetName = parentStorageDef.getStorageSetDef().getName();
            storageManager.removeStorage(storageName, true);
            storageManager.createStorage(storageSetName, storageDef);

            DataResult<ContentMetaDataDef> dr = contentMetaDataDao.findAll(parentStorageDef, 0, 10);

            long count = dr.getCount();

            logger.info("fetching " + count + " record for rebuilding");

            // set size to callback
            storageProgressWalker.setSize(count);

            int record = 1;
            int offset = 0;
            int limit = 10;
            boolean doMore = true;

            while (doMore) {

                DataResult<ContentMetaDataDef> dataResult = contentMetaDataDao.findAll(parentStorageDef, offset, limit);

                offset = offset + limit;
                if (dataResult.getResultSize() == 0) doMore = false;


                for (ContentMetaDataDef cm : dataResult.getList()) {

                    storageProgressWalker.onProgress(record, count);

                     logger.info("recreating record: " + record + " from " + count);

                    File fileTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".tmp");
                    try {
                        retriveContent(fileTmpFile, cm.getUuid(), parentStorageDef.getName(), false);
                    } catch (Exception e) {
                        logger.warn("Retriveing content " + cm.getUuid() + " has failed. Please run consistency Job before recreating storage content!"
                        );
                    }

                    if(fileTmpFile.exists()) {
                        writeContent(cm.getFileName(), fileTmpFile, storageDef, cm.getUuid());
                    }else{
                        logger.warn("File doesnt exists: " +fileTmpFile);
                    }
                    record++;

                }

            }


        } catch (StorageManagerException e) {
            String errMessage = "Error recreating storage " + storageName + ".";
            logger.error(errMessage, e);
            throw new StorageManagerException(errMessage);
        } catch (ContentMetaDataDaoException e) {
            String errMessage = "Error recreating storage " + storageName + ".";
            logger.error(errMessage, e);
            throw new StorageManagerException(errMessage);
        } catch (ContentManagerException e) {
            String errMessage = "Error recreating storage " + storageName + ".";
            logger.error(errMessage, e);
            throw new StorageManagerException(errMessage);
        } catch (IOException e) {
            String errMessage = "Error recreating storage " + storageName + ".";
            logger.error(errMessage, e);
            throw new StorageManagerException(errMessage);
        }

    }
}
